﻿#
# Script.ps1
#

Set-StrictMode -Version 3
$ErrorActionPreference = "Stop"

# Writes $manifest to output file $fileName, overwriting any existing file.
# Output is written for human consumption (with whitespace and indentation)
function saveManifestToFile([xml] $manifest, $fileName) {

    # create the XML file that we're working with.
    $output = New-Object -TypeName System.Text.StringBuilder

    $settings = New-Object -TypeName System.Xml.XmlWriterSettings
    $settings.Indent = $true
    $settings.NamespaceHandling = [System.Xml.NamespaceHandling]::OmitDuplicates
    
    $writer = [System.Xml.XmlWriter]::Create($output, $settings)

    $manifest.WriteTo($writer)
    $writer.Flush()
    $writer.Close()

    $s = $output.ToString()

    $s | set-content -path $fileName
}

# Loads the file .ignore in $bspRoot and returns an array of the lines in the 
# file
function loadIgnoreFile($bspRoot) {

    $fullName = Join-Path $bspRoot ".ignore"
    $ignoreList = @{}
    if (Test-Path $fullName -PathType Leaf) {
        Get-Content -Path $fullName | 
            ForEach-Object {
                if     ($_.StartsWith("\"))  { $_ = $_.Substring(1) } 
                elseif ($_.StartsWith(".\")) { $_ = $_.Substring(2) }
                $ignoreList[$_] = $false 
            }
    }
    
    return $ignoreList
}

$ItemTypeToXmlTable =@{Driver=@("PackageFile", $null);
                       Firmware=@("FirmwarePackage", $null);
                       WindowsTool=@("ToolPackage", "Windows");
                       UefiTool=@("ToolPackage", "Uefi")}

# Creates an XmlElement for a package, based on relative the path to the INF 
# ($infPath).  $xmlDoc is used to create the elements in the proper document
# returns the created element.  Caller must insert it
function itemToXmlElement{
    param (
        [parameter(ValueFromPipeline=$true)]
        [psobject[]]
        $Item,
        [xml] 
        $xmlDoc
        )

    process {
        if ($item -eq $null) {return}

        $typeInfo = $ItemTypeToXmlTable[$item.Type]

        $node = $xmlDoc.CreateElement($null, $typeInfo[0], "DevboardBspManifestSchema.xsd")
        $pathAttribute = $xmlDoc.CreateAttribute("Path");

        $pathAttribute.Value = Join-Path "%PKGDIR%" (Split-Path -Parent $Item.Path)

        $nameAttribute = $xmlDoc.CreateAttribute("Name")
        $nameAttribute.Value = $Item.Name

        $node.Attributes.Append($pathAttribute) > $null
        $node.Attributes.Append($nameAttribute) > $null

        if ($typeInfo[1] -ne $null) {
            $typeAttribute = $xmlDoc.CreateAttribute("Type")
            $typeAttribute.Value = $typeInfo[1]
            $node.Attributes.Append($typeAttribute) > $null
        }
   
        return $node
    }
}

# Processes the BSP directory, converting each INF file found under it 
# into an element in the returned manifest XmlDocument.
function importBspFromRoot{
    param ($bsp)

    Push-Location $bsp.Root
    try {

    # parse the ignore file, so we know which elements to ignore
    [hashtable] $ignoreFile = loadIgnoreFile .

    # get a list of INF files located in the BSP source directory
    Get-ChildItem -Recurse -Filter *.inf -Path . |
        Resolve-Path -Relative |
        ForEach-Object {

            $name = ([string]$_).Replace(".\", "")

            if ($ignoreFile.Keys -contains $name) {
                $ignoreFile[$name] = $true
                return
            }

            $bsp.AddContent("Driver", $_)
        }

        # write a warning message for each ignore file entry that didn't match
        $ignoreFile.GetEnumerator() | 
            Where-Object {$_.Value -eq $false} | 
            ForEach-Object {
                Write-Warning "Ignore file entry $($_.Key) did not match any files during import"
            }

    } finally {
        Pop-Location
    }
}

function getRootRelativePath([string] $root, [string] $ItemPath) {
    if (Split-Path -IsAbsolute -Path $ItemPath) {
        if ($ItemPath.ToLower().StartsWith($root.ToLower()) -eq $false) {
            throw "Absolute `$ItemPath must be under Root"
        }

        return $ItemPath.Substring($root.Length)
    } elseif ($ItemPath.StartsWith(".")) {
        return $ItemPath.Substring(2)
    } else {
        return $ItemPath
    }
}

function New-BspManifest {
<#
.SYNOPSIS

Creates a new BspManifest object

.DESCRIPTION

This cmdlet creates a new BspManifest object, optionally importing any INF
files found within the directory.  The resulting object provides methods 
for adding additional content and then generating an XML manifest for the 
board support package.

.PARAMETER RootDirectory

The root directory of the board support package.  All content added to the manifest must be under this directory.

.PARAMETER Import

An optional switch to search for driver packages (INF files) and add each driver found into the manifest object before returning it.

.NOTES

The resulting object provides methods and properties for modifying the manifest content:

  * Drivers 
      Provides a list of the driver packages added to the manifest

  * Save($Destination)
      Writes an XML file containing the manifest, for use with 
      New-DevboardImage

  * Import($SourceDirectory)
      Imports all the drivers found in the specified directory.
  
  * AddContent($ContentType, $ItemPath)
      Adds a specific item (file or directory) the manifest.  ContentType
      indicates how the item should be applied to the target image.

  * RemoveContent($ItemPath) 
      Removes an item added with AddContent or imported with Import

.LINK

    Create-DevboardImage.ps1

    New-DevboardImage
#>
    param(
        [parameter(Mandatory=$true)]
        [string] $RootDirectory,
        [switch] $Import
    )

    $bsp = New-Object -TypeName psobject

    # Root directory should always end with \ so that the relative paths 
    # we create don't start with \
    if ($RootDirectory.EndsWith("\") -eq $false) {
        $RootDirectory = "$RootDirectory\"
    }

    if ((Test-Path -PathType Container $RootDirectory) -eq $false) {
        throw "`$RootDirectory does not exist, or is not a directory"
    }

    Add-Member -InputObject $bsp -MemberType NoteProperty -Name "Items" -value @{}

    Add-Member -InputObject $bsp -MemberType NoteProperty -Name "Root" -value (Resolve-Path $RootDirectory)
    
    Add-Member -InputObject $bsp -MemberType ScriptProperty -Name "XmlDocument" -value `
    {
        # get
        $_boilerPlate = @"
<?xml version="1.0" encoding="utf-8" ?> 
<!-- Generated by New-BspManifest.ps1 -->
<FeatureManifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="DevboardBspManifestSchema.xsd">
<BasePackages/>
<Features>
    <OEM/>
</Features>
</FeatureManifest>
"@
        $doc = [xml] $_boilerPlate
        $doc.PreserveWhitespace = $true

        # add all drivers
        $parent = $doc.FeatureManifest.GetElementsByTagName("BasePackages")[0]
        $this.Items.Values | 
            Where-Object {$_.Type -eq "Driver"} |
            itemToXmlElement -xmlDoc $doc |
            ForEach-Object { $parent.AppendChild($_) } |
            Out-Null

        $parent = $doc.FeatureManifest.Features.GetElementsByTagName("OEM")[0]
        $this.Items.Values | 
            Where-Object {$_.Type -ne "Driver"} |
            itemToXmlElement -xmlDoc $doc |
            ForEach-Object { $parent.AppendChild($_) } |
            Out-Null

        return $doc
    }

    Add-Member -InputObject $bsp -MemberType ScriptProperty -Name "Drivers" -Value {
        $t = @{}
        
         $this.Items.GetEnumerator() |
            Where-Object {$_.Value.Type -eq "Driver"} | 
            ForEach-Object {$t[$_.Value.Name] = $_.Value.Path}

        return $t
    }
    
    Add-Member -InputObject $bsp -MemberType ScriptMethod -Name "Save" -Value {
        param([parameter(Mandatory=$true)] $Destination)
        $doc = $this.XmlDocument
        saveManifestToFile $doc $Destination
    }

    Add-Member -InputObject $bsp -MemberType ScriptMethod -Name "Import" -Value {
        param([parameter(Mandatory=$true)] $SourceDirectory) 
        $this.Document = processBspDirectory $SourceDirectory
    }

    Add-Member -InputObject $bsp -MemberType ScriptMethod -Name "AddContent" -Value {
        param(
            [parameter(Mandatory=$true)]
            [ValidateSet("Driver", "Firmware", "WindowsTool", "UefiTool")]
            $ContentType,

            [parameter(Mandatory=$true)] 
            [string] $ItemPath
            )

        $p = getRootRelativePath -Root $this.Root -ItemPath $ItemPath
        $name = Split-Path -Path $p -Leaf

        if ($this.Items.Keys -contains $name) {
            Write-Warning "Overwriting item $name entry [$($this.Items[$name])]"
        }

        $item = New-Object -TypeName psobject -Property @{
                                                Name=$name;
                                                Type=$ContentType;
                                                Path=$p
                                                }

        $this.Items[$name] = $item
    }

    Add-Member -InputObject $bsp -MemberType ScriptMethod -Name "RemoveContent" -Value {
        param([parameter(Mandatory=$true)] [string] $ItemPath)

        $InfPath = getRootRelativePath -ItemPath $ItemPath -root $this.Root
        $name = Split-Path -Path $InfPath -Leaf

        if ($this.Items.Keys -notcontains $name) {
            Write-Warning "Driver $name not found"
        }

        $this.Items.Remove($name)
    }

    if ($Import.IsPresent) {
        importBspFromRoot -bsp $bsp
    }

    return $bsp
}
